/*
 * Jamus
 * Copyright (C) 2010 Jamus Team
 *
 * Jamus is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Jamus is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Jamus.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <boost/algorithm/string.hpp>
#include <signal.h>

#include "Plugin.h"
#include "../../NoCurrentTrackException.h"

using namespace std;

namespace {
	void cancelSignalHandler(int, siginfo_t*, void*) { /*ignore*/ }


	// Turns the given URL into a file name. The URL must have "file://" as protocol.
	string decodeUrl(const string& url)
	{
		string file;
		file.reserve(url.size());
		for(size_t i = 0; i < url.size(); ++i) {
			switch(url[i]) {
				case '+':
					file += ' ';
					break;

				case '%': {
					assert(i+3 <= url.size());
					int value = 0;
					istringstream iss(url.substr(i+1, 2));
					iss >> hex >> value;
					file += static_cast<char>(value);
					i += 2;
					break;
				}

				default:
					file += url[i];
					break;
			}
		}

		assert(boost::starts_with(file, "file://"));
		return file.substr(7);
	}
}

namespace backend {
namespace xmms2 {

log4cxx::LoggerPtr Plugin::logger(log4cxx::Logger::getLogger("backend.xmms2.Plugin"));
const int Plugin::CANCEL_SIGNAL = SIGUSR1;

Plugin::Plugin(bool useSeekSignalPlaytimeWorkaround)
: xmms("Jamus"),
  running(false),
  lastSignalledPlaytime(0)
{
	xmms.connect();

	// Fetch track list
	Xmms::List<int> allTrackIds = xmms.collection.queryIds(Xmms::Coll::Universe());

	for(Xmms::List<int>::const_iterator i = allTrackIds.begin(); i != allTrackIds.end(); ++i) {
		Track track;
		buildTrackFromId(track, *i);
		tracks.push_back(track);
	}

	// Fetch current playlist
	fetchPlaylistFromServer();

	// Connect signals
	idleConnection.reset(new Xmms::Client("Jamus_idle"));
	idleConnection->connect();
	idleConnection->playlist.broadcastChanged()(Xmms::bind(&Plugin::onPlaylistChanged, this));
	idleConnection->playlist.broadcastCurrentPos()(Xmms::bind(&Plugin::onCurrentPosChanged, this));
	idleConnection->playback.broadcastStatus()(Xmms::bind(&Plugin::onPlaybackStatusChanged, this));
	idleConnection->playback.broadcastVolumeChanged()(Xmms::bind(&Plugin::onVolumeChanged, this));

	if(useSeekSignalPlaytimeWorkaround)
		idleConnection->playback.signalPlaytime()(Xmms::bind(&Plugin::onPlaytimeChanged, this));

	LOG4CXX_INFO(logger, "XMMS2 back-end ready.");
}

Plugin::~Plugin()
{
	LOG4CXX_TRACE(logger, "Destroy Plugin");
}

void Plugin::run()
{
	assert(!running);

	xmmsc_connection_t* const conn = idleConnection->getConnection();
	const int connFd = xmmsc_io_fd_get(conn);

	// We need a reference to this thread when another calls cancel()
	runThread = pthread_self();
	running = true;

	// We want to be able to interrupt select() by sending a signal from
	// cancel().
	sigset_t sigmask;
	sigfillset(&sigmask);
	sigdelset(&sigmask, CANCEL_SIGNAL);
	// When it is received, ignore it (otherwise the program would terminate)
	struct sigaction action;
	action.sa_handler = 0; // to make valgrind warnings go away
	action.sa_sigaction = &cancelSignalHandler;
	sigemptyset(&action.sa_mask); // to make valgrind warnings go away
	action.sa_flags = 0; // to make valgrind warnings go away

	sigaction(CANCEL_SIGNAL, &action, 0);

	for(;;) {
		// Wait until data can be read/written from/to the XMMS idle connection
		// or until the cancel signal is caught
		fd_set rfds;
		fd_set wfds;
		FD_ZERO(&rfds);
		FD_ZERO(&wfds);
		FD_SET(connFd, &rfds);
		if(xmmsc_io_want_out(conn))
			FD_SET(connFd, &wfds);

		// pselect() unblocks the cancel signal only as long as it runs
		if(pselect(connFd + 1, &rfds, &wfds, 0, 0, &sigmask) < 0) {
			if(errno == EINTR && !running)
				break; // cancel() called

			// Some other error occured
			throw runtime_error(strerror(errno));
		}

		assert(FD_ISSET(connFd, &rfds) || FD_ISSET(connFd, &wfds));

		if(FD_ISSET(connFd, &rfds))
			xmmsc_io_in_handle(conn);

		if(FD_ISSET(connFd, &wfds) && xmmsc_io_want_out(conn))
			xmmsc_io_out_handle(conn);
	}

	action.sa_handler = SIG_DFL;
	sigaction(CANCEL_SIGNAL, &action, 0);
}

void Plugin::cancel()
{
	LOG4CXX_TRACE(logger, "cancel");

	if(!running)
		return;

	running = false;
	// Send signal to the thread in run() to interrupt pselect()
	if(pthread_kill(runThread, CANCEL_SIGNAL))
		LOG4CXX_ERROR(logger, "Could not send signal to thread in run()");

	idleConnection.reset();
}

Plugin::PlaybackStatus Plugin::getPlaybackStatus()
{
	boost::lock_guard<boost::mutex> lock(mutex);

	LOG4CXX_TRACE(logger, "getPlaybackStatus");
	const Xmms::Playback::Status s = xmms.playback.getStatus();

	switch(s) {
		case Xmms::Playback::STOPPED:
			return STOP;
		case Xmms::Playback::PLAYING:
			return PLAY;
		case Xmms::Playback::PAUSED:
			return PAUSE;
	}

	LOG4CXX_WARN(logger, "Unknown playback status: " << s << " -- returning STOP");
	return STOP;
}

void Plugin::play()
{
	boost::lock_guard<boost::mutex> lock(mutex);

	xmms.playback.start();
}

void Plugin::play(int track)
{
	boost::lock_guard<boost::mutex> lock(mutex);

	xmms.playlist.setNext(track);
	xmms.playback.tickle();
	xmms.playback.start();
}

void Plugin::pause()
{
	boost::lock_guard<boost::mutex> lock(mutex);

	xmms.playback.pause();
}

void Plugin::playPause()
{
	boost::lock_guard<boost::mutex> lock(mutex);

	if(xmms.playback.getStatus() != Xmms::Playback::PLAYING)
		xmms.playback.start();
	else
		xmms.playback.pause();
}

void Plugin::stop()
{
	boost::lock_guard<boost::mutex> lock(mutex);

	xmms.playback.stop();
}

void Plugin::nextTrack()
{
	boost::lock_guard<boost::mutex> lock(mutex);

	xmms.playlist.setNextRel(1);
	xmms.playback.tickle();
}

void Plugin::prevTrack()
{
	boost::lock_guard<boost::mutex> lock(mutex);

	xmms.playlist.setNextRel(-1);
	xmms.playback.tickle();
}

int Plugin::getCurrentTrack()
{
	boost::lock_guard<boost::mutex> lock(mutex);

	try {
		Xmms::Dict result = xmms.playlist.currentPos();

		LOG4CXX_TRACE(logger, "currentTrack: " << result.get<int>("position"));

		return result.get<int>("position");
	} catch(Xmms::result_error&) {
		throw NoCurrentTrackException();
	}
	return 0;
}

boost::posix_time::time_duration Plugin::getElapsed()
{
	boost::lock_guard<boost::mutex> lock(mutex);

	LOG4CXX_TRACE(logger, "getElapsed");
	return boost::posix_time::milliseconds(xmms.playback.getPlaytime());
}

void Plugin::seek(const boost::posix_time::time_duration& pos)
{
	boost::lock_guard<boost::mutex> lock(mutex);

	LOG4CXX_TRACE(logger, "seek");

	// XXX: Throw NoCurrentTrackException if no current song
	xmms.playback.seekMs(pos.total_milliseconds());
}

int Plugin::getVolume()
{
	boost::lock_guard<boost::mutex> lock(mutex);

	LOG4CXX_TRACE(logger, "getVolume");

	try {
		Xmms::Dict result = xmms.playback.volumeGet();

		const int l = result.get<int>("left");
		const int r = result.get<int>("right");

		if(l != r)
			LOG4CXX_WARN(logger, "Volume for left and right channel different");

		return l;
	} catch(Xmms::result_error& e) {
		LOG4CXX_ERROR(logger, "XMMS2 could not report volume: " << e.what());
	}
	LOG4CXX_WARN(logger, "getVolume failed -- returning volume 0");

	return 0;
}

void Plugin::setVolume(int vol)
{
	boost::lock_guard<boost::mutex> lock(mutex);

	xmms.playback.volumeSet("left", vol);
	xmms.playback.volumeSet("right", vol);
}

const Plugin::TrackList& Plugin::getTrackList()
{
	return tracks;
}

const Plugin::Playlist& Plugin::getPlaylist()
{
	return playlist;
}

void Plugin::addToPlaylist(const std::string& fileName)
{
	boost::lock_guard<boost::mutex> lock(mutex);

	xmms.playlist.addUrl("file://" + fileName); // addUrl() will encode the URL properly. (There is by the way also addUrlEncoded())
}

void Plugin::removeFromPlaylist(int index)
{
	boost::lock_guard<boost::mutex> lock(mutex);

	xmms.playlist.removeEntry(index);
}

void Plugin::clearPlaylist()
{
	boost::lock_guard<boost::mutex> lock(mutex);

	xmms.playlist.clear();
}

void Plugin::buildTrackFromId(Track& track, int id)
{
	Xmms::PropDict info = xmms.medialib.getInfo(id);

	try {
		const string url = info.get<string>("url");
		assert(boost::starts_with(url, "file://"));
		track.setFileName(decodeUrl(url));
	} catch(Xmms::no_such_key_error&) {}

	try {
		track.setArtist(info.get<string>("artist"));
	} catch(Xmms::no_such_key_error&) {}

	try {
		track.setAlbum(info.get<string>("album"));
	} catch(Xmms::no_such_key_error&) {}

	try {
		track.setTitle(info.get<string>("title"));
	} catch(Xmms::no_such_key_error&) {}

	try {
		track.setTrackNumber(info.get<int>("tracknr"));
	} catch(Xmms::no_such_key_error&) {}

	try {
		track.setDuration(boost::posix_time::milliseconds(info.get<int>("duration")));
	} catch(Xmms::no_such_key_error&) {}
}

void Plugin::fetchPlaylistFromServer()
{
	Xmms::List<int> playlistTrackIds = xmms.playlist.listEntries();

	playlist.clear();

	for(Xmms::List<int>::const_iterator i = playlistTrackIds.begin(); i != playlistTrackIds.end(); ++i) {
		Track track;
		buildTrackFromId(track, *i);
		playlist.push_back(track);
	}
}

bool Plugin::onCurrentPosChanged(const Xmms::Dict& dict)
{
	LOG4CXX_DEBUG(logger, "onCurrentPosChanged: " << dict["position"]);
	sigPlaybackStatusChanged();
	return true;
}

bool Plugin::onPlaylistChanged(const Xmms::Dict& dict)
{
	LOG4CXX_DEBUG(logger, "onPlaylistChanged");
	{
		boost::lock_guard<boost::mutex> lock(mutex);
		fetchPlaylistFromServer();
	}
	sigPlaylistChanged();
	return true;
}

bool Plugin::onPlaybackStatusChanged(const xmms_playback_status_t&)
{
	LOG4CXX_DEBUG(logger, "onPlaybackStatusChanged");
	sigPlaybackStatusChanged();
	return true;
}

bool Plugin::onVolumeChanged(const Xmms::Dict& dict)
{
	LOG4CXX_DEBUG(logger, "onVolumeChanged");
	sigVolumeChanged();
	return true;
}

bool Plugin::onPlaytimeChanged(const int& time)
{
	// XXX: Unfortunately this gets fired periodically, not only when a seek occured.
	// However, I currently see no other way to be informed of seeks!?
	LOG4CXX_DEBUG(logger, "onPlaytimeChanged: " << time);

	bool fire = false; // Fire the signal?

	const boost::posix_time::ptime now = boost::posix_time::microsec_clock::local_time();

	// Fire signal if timeOfLastPlaytimeSignal was not set yet
	if(timeOfLastPlaytimeSignal.is_not_a_date_time()) {
		LOG4CXX_TRACE(logger, "timeOfLastPlaytimeSignal not set -- firing signal and setting it");
		fire = true;
	}

	else {
		const boost::posix_time::time_duration timeSinceLastSignal = now - timeOfLastPlaytimeSignal;

		const int estimatedTime = lastSignalledPlaytime + timeSinceLastSignal.total_milliseconds();

		// Only fire signal if time is significantly adrift from estimate (also happens after a large enough seek obviously)
		if(abs(time - estimatedTime) >= 100) {
			LOG4CXX_TRACE(logger, "Time " << time << " is significantly adrift from estimate " << estimatedTime);
			fire = true;
		}
	}

	timeOfLastPlaytimeSignal = now;
	lastSignalledPlaytime = time;

	if(fire)
		sigPlaybackStatusChanged();

	return true;
}

} // namespace xmms2
} // namespace backend

extern "C" backend::xmms2::Plugin* create(const boost::property_tree::ptree& config)
{
	return new backend::xmms2::Plugin(config.get("config.backend.xmms2.seek_signal_playtime_workaround", true));
}

extern "C" void destroy(backend::xmms2::Plugin* p)
{
	delete p;
}
